/*
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */

package org.evosuite.testcase;

import org.evosuite.testcase.execution.TestCaseExecutor;
import org.evosuite.testcase.statements.*;
import org.evosuite.testcase.variable.ArrayIndex;
import org.evosuite.testcase.variable.ArrayReference;
import org.evosuite.testcase.variable.VariableReference;
import org.evosuite.utils.generic.GenericClassFactory;

import java.util.*;

public class TestCaseExpander {

    private final Set<VariableReference> usedVariables = new HashSet<>();

    public Map<Integer, Set<VariableReference>> variableMapping = new HashMap<>();

    private int currentPosition = 0;

    public TestCase expandTestCase(TestCase test) {
        TestCase expandedTest = test.clone();
        // Deactivated for now - only needed in NL branch
        // createConcretePrimitives(expandedTest);
        while (currentPosition < expandedTest.size()) {
            Statement statement = expandedTest.getStatement(currentPosition);
            if (statement instanceof MethodStatement) {
                visitMethodStatement(expandedTest, (MethodStatement) statement);
            } else if (statement instanceof ConstructorStatement) {
                visitConstructorStatement(expandedTest, (ConstructorStatement) statement);
            } else if (statement instanceof ArrayStatement) {
                visitArrayStatement(expandedTest, ((ArrayStatement) statement));
            } else if (statement instanceof AssignmentStatement) {
                visitAssignmentStatement(expandedTest, ((AssignmentStatement) statement));
            }
            currentPosition++;
        }
        return expandedTest;
    }

    @SuppressWarnings({"rawtypes", "unchecked", "unused"})
    private void createConcretePrimitives(TestCase test) {

        // Execute test to collect concrete values
        TestCaseExecutor executor = TestCaseExecutor.getInstance();
        ConcreteValueObserver observer = new ConcreteValueObserver();
        executor.addObserver(observer);
        executor.execute(test);
        executor.removeObserver(observer);

        // Now replace references to concrete values with new primitive statements
        Map<Integer, Object> concreteValues = observer.getConcreteValues();
        List<Integer> positions = new ArrayList<>(concreteValues.keySet());
        positions.sort(Collections.reverseOrder());

        for (Integer position : positions) {
            Object value = concreteValues.get(position);
            Statement statement = test.getStatement(position);

            PrimitiveStatement primitive = PrimitiveStatement.getPrimitiveStatement(test,
                    GenericClassFactory.get(
                            value.getClass()));
            primitive.setValue(value);
            VariableReference replacement = test.addStatement(primitive, position);
            test.replace(statement.getReturnValue(), replacement);
        }
    }

    private VariableReference duplicateStatement(TestCase test, VariableReference owner) {
        Statement statement = test.getStatement(owner.getStPosition());
        currentPosition++;
        VariableReference copy = test.addStatement(statement.clone(test),
                owner.getStPosition() + 1);
        if (!variableMapping.containsKey(owner.getStPosition())) {
            variableMapping.put(owner.getStPosition(), new HashSet<>());
            // variableMapping.get(owner.getStPosition()).add(owner);
        }
        variableMapping.get(owner.getStPosition()).add(copy);
        return copy;
    }

    private void addUnchangedMapping(TestCase test, VariableReference var) {
        VariableReference copy = test.getStatement(var.getStPosition()).getReturnValue();
        if (!variableMapping.containsKey(var.getStPosition())) {
            variableMapping.put(var.getStPosition(), new HashSet<>());
            variableMapping.get(var.getStPosition()).add(var);
        }
        variableMapping.get(var.getStPosition()).add(copy);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.evosuite.testcase.TestVisitor#visitMethodStatement(org.evosuite
     * .testcase.MethodStatement)
     */
    public void visitMethodStatement(TestCase test, MethodStatement statement) {
        // The problem is that at this point in the test case the parameters
        // might have already changed

        int i = 0;
        for (VariableReference var : statement.getParameterReferences()) {
            if (var.isPrimitive() || var.isString()) {
                if (usedVariables.contains(var)
                        && test.getStatement(var.getStPosition()) instanceof PrimitiveStatement<?>) {
                    // Duplicate and replace
                    VariableReference varCopy = duplicateStatement(test, var);
                    statement.replaceParameterReference(varCopy, i);
                    usedVariables.add(varCopy);
                }
                usedVariables.add(var);
            }
            i++;
        }
        addUnchangedMapping(test, statement.getReturnValue());
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.evosuite.testcase.TestVisitor#visitConstructorStatement(org.evosuite
     * .testcase.ConstructorStatement)
     */
    public void visitConstructorStatement(TestCase test, ConstructorStatement statement) {
        int i = 0;
        for (VariableReference var : statement.getParameterReferences()) {
            if (var.isPrimitive() || var.isString()) {
                if (usedVariables.contains(var)
                        && test.getStatement(var.getStPosition()) instanceof PrimitiveStatement<?>) {
                    // Duplicate and replace
                    VariableReference varCopy = duplicateStatement(test, var);
                    statement.replaceParameterReference(varCopy, i);
                    usedVariables.add(varCopy);
                }
                usedVariables.add(var);
            }
            i++;
        }
        addUnchangedMapping(test, statement.getReturnValue());

    }

    public void visitArrayStatement(TestCase test, ArrayStatement statement) {
        ArrayReference arrRef = (ArrayReference) statement.getReturnValue();

        Set<Integer> assignments = new HashSet<>();
        int position = statement.getPosition() + 1;

        while (position < test.size()) {
            Statement st = test.getStatement(position);
            if (st instanceof AssignmentStatement) {
                if (st.getReturnValue() instanceof ArrayIndex) {
                    ArrayIndex arrayIndex = (ArrayIndex) st.getReturnValue();
                    if (arrayIndex.getArray().equals(arrRef)) {
                        assignments.add(arrayIndex.getArrayIndex());
                    }
                }
            } else if (st instanceof PrimitiveStatement) {
                // OK, ignore
            } else {
                break;
            }
            position++;
        }

        position = statement.getPosition() + 1;

        for (int i = 0; i < statement.size(); i++) {
            if (assignments.contains(i))
                continue;

            ArrayIndex index = new ArrayIndex(test, arrRef, i);
            VariableReference retVal = null;
            if (index.isPrimitive()) {
                PrimitiveStatement<?> primitive = PrimitiveStatement.getPrimitiveStatement(test,
                        index.getGenericClass());
                retVal = test.addStatement(primitive, position++);
            } else {
                NullStatement nullStatement = new NullStatement(test, index.getType());
                retVal = test.addStatement(nullStatement, position++);
            }
            AssignmentStatement assignment = new AssignmentStatement(test, index, retVal);
            test.addStatement(assignment, position++);

        }
    }

    public void visitAssignmentStatement(TestCase test, AssignmentStatement statement) {

        VariableReference var = statement.getValue();
        if (var.isPrimitive() || var.isString()) {
            if (usedVariables.contains(var)
                    && test.getStatement(var.getStPosition()) instanceof PrimitiveStatement<?>) {
                // Duplicate and replace
                VariableReference varCopy = duplicateStatement(test, var);
                statement.replace(var, varCopy);
                usedVariables.add(varCopy);
            }
            usedVariables.add(var);
        }
        addUnchangedMapping(test, statement.getReturnValue());

    }

}